/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2008 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Collections.Generic;
using System.Security.Cryptography;
using Mono.Security.Protocol.Tls;

using Galaxium.Core;
using Galaxium.Client;

using Anculus.Core;

namespace Galaxium.Protocol.GaduGadu
{
	public sealed class GaduSession : AbstractSession
	{
	//	private bool _disposed;
	//	private ContactCollection _pendingContacts;
		private ContactCollection _contacts;
		private GroupCollection _groups;
		private GaduConnection _connection;
		private GaduGroup _empty_group;
	//	private uint _seed;
		
		public override GroupCollection GroupCollection
		{
			get { return _groups; }
		}
		
		public IEnumerable<IGroup> Groups
		{
			get { return _groups; }
		}
		
		public override ContactCollection ContactCollection
		{
			get { return _contacts; }
		}
		
		internal GaduConnection Connection
		{
			get { return _connection; }
		}
		
		public GaduSession (GaduAccount account) : base (account)
		{
		//	_pendingContacts = new ContactCollection ();
			_contacts = new ContactCollection ();
			_groups = new GroupCollection ();
			
			Conversations = new GaduConversationManager ();
			
			_empty_group = new GaduGroup (this, "0", "Other Contacts");
			
			_groups.Add (_empty_group);
			
			account.Presence = account.InitialPresence;
		}
		
		protected override void Dispose (bool disposing)
		{
	//		_disposed = true;
		}
		
		public override void Connect ()
		{
			GaduAccount account = Account as GaduAccount;
			
			if (_connection == null)
			{
				BeginConnect (account);
			}
		}
		
		private void BeginConnect (GaduAccount account)
		{
			string uri = "http://appmsg.gadu-gadu.pl/appsvc/appmsg4.asp?fmnumber="+account.UniqueIdentifier;
			
			this.EmitLoginProgress (new SessionProgressEventArgs (this, "Searching...", 0.0));
			
			Log.Debug ("Obtaining gadu connection information...");
			
			try
			{
				HttpWebRequest request = (HttpWebRequest) HttpWebRequest.Create (uri);
				request.Method = "GET";
				
				request.BeginGetResponse (BeginConnectComplete, request);
			}
			catch (WebException e)
			{
				Log.Error ("Unable to obtain connection information: "+e.Message);
			}
		}
		
		private void BeginConnectComplete (IAsyncResult ar)
		{
			string host = String.Empty;
			int port = 0;
			
			try
			{
				HttpWebRequest request = (HttpWebRequest)ar.AsyncState;
				HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(ar);
				Stream stream = response.GetResponseStream ();
				
				StreamReader reader = new StreamReader(stream);
				string line = reader.ReadLine();
				response.Close();
				
				ThrowUtility.ThrowIfEmpty ("line", line);
				
				String[] vals;
				vals = line.Split(' ');
				
				if (vals[0] != "0")
				    Log.Error ("Invalid arguments in the server connection information request.");
				
				line = vals[2];
				vals = line.Split(':');
				host = vals[0];
				port = Convert.ToInt32(vals[1]);
			}
			catch (WebException e)
			{
				Log.Error ("Unable to complete connection information:" +e.Message);
				return;			}
			
			try
			{
				Log.Debug ("Creating gadu connection...");
				this.EmitLoginProgress (new SessionProgressEventArgs (this, "Connecting...", 0.0));
				
				_connection = new GaduConnection (this, new GaduConnectionInfo (host, port, false));
				
				_connection.Established += ConnectionEstablished;
				_connection.LoginOk += ConnectionLoginOk;
				_connection.LoginFailed += ConnectionLoginFailed;
				_connection.MessageReceived += ConnectionMessageReceived;
				_connection.MessageAck += ConnectionMessageAck;
				_connection.Pong += ConnectionPong;
				_connection.NeedEmail += ConnectionNeedEmail;
				_connection.UserListEntry += ConnectionUserListEntry;
				_connection.UserListComplete += ConnectionUserListComplete;
				_connection.UserStatusUpdate += ConnectionUserStatusUpdate;
				_connection.UserInitialStatusUpdate += ConnectionUserInitialStatusUpdate;
				
				Log.Debug ("Connecting...");
				
				_connection.Connect ();
				
				this.EmitLoginProgress (new SessionProgressEventArgs (this, "Authorizing...", 0.33));
				
				OnConnected (new SessionEventArgs (this));
			}
			catch (Exception e)
			{
				Log.Debug ("Unable to connect: "+e.Message);
				return;
			}
		}
		
		private void ConnectionLoginOk (object sender, EventArgs args)
		{
			this.EmitLoginProgress (new SessionProgressEventArgs (this, "Synchronizing...", 0.66));
			
			Log.Debug ("Setting initial presence status.");
			SetPresenceStatus ((Account as GaduAccount).InitialPresence, Account.DisplayMessage);
			
			// Now lets see who we want to notify.
			if (this._contacts.Count == 0)
			{
				Log.Debug ("Sending out empty list.");
				
				EmptyListCommand cmd = new EmptyListCommand ();
				
				_connection.Send (cmd, false);
				
				// Attempt to obtain the userlist.
				RequestUserList ();
			}
			else
			{
				Log.Debug ("Sending out notify for known contacts.");
				
				NotifyLastCommand notify = new NotifyLastCommand ();
				foreach (GaduContact contact in _contacts)
					notify.Entries.Add (Convert.ToInt32(contact.UniqueIdentifier), GaduConstants.USER_NORMAL);
				
				_connection.Send (notify, false);
			}
		}
		
		private void ConnectionLoginFailed (object sender, EventArgs args)
		{
			Log.Debug ("User login failed.");
			
			EmitErrorOccurred (new ErrorEventArgs (this, "Login Failed", "Authentication Failed!"));
		}
		
		private void ConnectionPong (object sender, EventArgs args)
		{
			
		}
		
		private void ConnectionNeedEmail (object sender, EventArgs args)
		{
			
		}
		
		private void ConnectionMessageReceived (ReceiveMessageCommand msg)
		{
			GaduContact contact = _contacts.GetContact (msg.Sender.ToString()) as GaduContact;
			
			if (contact != null)
			{
				// Here we would check for existing conversations with the user id
				GaduConversation conv = Conversations.GetConversation (contact) as GaduConversation;
				
				if (conv == null)
				{
					Log.Debug ("Creating conversation for gadu contact: " + msg.Sender);
					
					conv = new GaduConversation (contact, this);
					
					Conversations.Add (conv);
					
					EmitConversationInitiated (new ConversationEventArgs (conv));
				}
				
				conv.IncomingMessage (msg.Sender, msg.Message, msg.Time, msg.Class);
			}
			else
			{
				Log.Error ("Contact that messaged us is not in list: " + msg.Sender);
			}
		}
		
		private void ConnectionMessageAck (SendMessageAckCommand ack)
		{
			
		}
		
		private void ConnectionEstablished (object sender, ConnectionEventArgs args)
		{
			Log.Debug ("Connection established.");
			
			Login (Int32.Parse(Account.UniqueIdentifier), Account.Password);
		}
		
		private void ConnectionUserListEntry (UserListEntry entry)
		{
			GaduContact contact = new GaduContact (this, entry.Number);
			contact.Nickname = entry.Nickname;
			contact.DisplayName = entry.AltNick;
			
			_contacts.Add (contact);
			_empty_group.Add (contact);
			
			EmitContactAdded (new ContactListEventArgs (contact, _empty_group));
		}
		
		private void ConnectionUserListComplete (object sender, EventArgs args)
		{
			// Now we are connected, lets start setting some stuff up.
			Log.Debug ("User logged in successfully.");
			
			ThreadUtility.Check ();
			
			ThreadUtility.SyncDispatch (new VoidDelegate (delegate {
				this.EmitLoginCompleted (new SessionEventArgs (this));
			}));
			
		}
		
		private void ConnectionUserInitialStatusUpdate (GetInitialStatusCommand cmd)
		{
			GaduContact contact = _contacts.GetContact (cmd.Number.ToString()) as GaduContact;
			
			if (contact != null)
			{
				IPresence oldstatus = contact.Presence;
				contact.Presence = GaduPresence.ParseGaduPresence ((int)cmd.Status);
				contact.DisplayMessage = cmd.Description;
				
				EmitContactPresenceChanged (new EntityChangeEventArgs<IPresence> (contact, contact.Presence, oldstatus));				
			}
			else
			{
				Log.Debug ("Contact status updated, but not found in memory: "+cmd.Number);
			}
		}
		
		private void ConnectionUserStatusUpdate (GetStatusCommand cmd)
		{
			GaduContact contact = _contacts.GetContact (cmd.Number.ToString()) as GaduContact;
			
			if (contact != null)
			{
				IPresence oldstatus = contact.Presence;
				contact.Presence = GaduPresence.ParseGaduPresence ((int)cmd.Status);
				contact.DisplayMessage = cmd.Description;
				
				ActivityUtility.EmitActivity (this, new EntityPresenceChangeActivity(contact, oldstatus));
				
				EmitContactPresenceChanged (new EntityChangeEventArgs<IPresence> (contact, contact.Presence, oldstatus));				
			}
			else
			{
				Log.Debug ("Contact status updated, but not found in memory: "+cmd.Number);
			}
		}
		
		public override void Disconnect ()
		{
			OnDisconnected (new SessionEventArgs (this));
		}
		
		private void Login (int number, string password)
		{
			Log.Debug ("Sending login command.");
			
			LoginCommand command = new LoginCommand ();
			command.Number = number;
			command.HashType = GaduConstants.GG_LOGIN_HASH_GG32;
			command.Hash = BitConverter.GetBytes(GaduCommand.CreateGG32Hash(password, _connection.Seed));
			
			//cmd.localIP = ((_connection.Connection as Galaxium.Protocol.TCPConnection).Socket.LocalEndPoint as IPEndPoint).Address;
			//cmd.LocalPort = (short)((_connection.Connection as Galaxium.Protocol.TCPConnection).Socket.LocalEndPoint as IPEndPoint).Port;
			//cmd.ExternalIP = ((_connection.Connection as Galaxium.Protocol.TCPConnection).Socket.RemoteEndPoint as IPEndPoint).Address;
			//cmd.ExternalPort = (short)((_connection.Connection as Galaxium.Protocol.TCPConnection).Socket.RemoteEndPoint as IPEndPoint).Port;
			
			_connection.Send (command, false);
		}
		
		private void RequestUserList ()
		{
			Log.Debug ("Requesting the user list.");
			
			UserListRequestCommand cmd = new UserListRequestCommand (GaduConstants.USERLIST_GET);
			
			_connection.Send (cmd, false);
	}
		
	// CHECKTHIS
	//	
	//	private void StoreUserList ()
	//	{
	//		List<UserListEntry> userlist = new List<UserListEntry> ();
	//		
	//		foreach (GaduContact contact in _contacts)
	//		{
	//			UserListEntry entry = new UserListEntry ();
	//			
	//			entry.Number = contact.UniqueIdentifier;
	//			entry.AltNick = contact.DisplayName;
	//			entry.Nickname = contact.Nickname;
	//			
	//			userlist.Add (entry);
	//		}
	//		
	//		UserListRequestCommand req = new UserListRequestCommand (GaduConstants.USERLIST_PUT);
	//		req.Entries = userlist;
	//		
	//	}
		
		public void AddContact (int uin)
		{
			GaduContact contact = new GaduContact (this, uin.ToString());
			
			AddNotifyCommand notify = new AddNotifyCommand ();
			
			notify.Entries.Add (uin, GaduConstants.USER_NORMAL);
			
			_empty_group.Add (contact);
			_contacts.Add (contact);
			
			EmitContactAdded (new ContactListEventArgs (contact, _empty_group));
		}
		
		public void SetPresenceStatus (IPresence presence, string message)
		{
			SetStatusCommand command = new SetStatusCommand (GaduConstants.STATUS_AVAIL_DESC, "Test Message");
			
			_connection.Send (command, false);
			
			// Actually modify the presence internally.
			Account.Presence = presence;
			Account.DisplayMessage = message;
		}
		
		public override void SetPresence (BasePresence presence)
		{
			IPresence pres = GaduPresence.Get(presence);
			
			if (pres != null)
				SetPresenceStatus(pres, Account.DisplayMessage);
			else
				Log.Warn("Cannot handle BasePresence {0}", presence);
		}
		
		public void SendMessage (int number, string message)
		{
			Log.Debug ("Sending a message to: "+number);
			
			SendMessageCommand msg = new SendMessageCommand (number, 0, GaduConstants.CLASS_MSG, message);
			
			_connection.Send (msg, false);
		}
		
	}
}